/**
 * 
 */
package com.googlecode.cswish.model;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;

import org.apache.commons.lang.ClassUtils;
import org.apache.log4j.Logger;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;

import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.struts.hibernate.IDynamic;
import com.googlecode.cswish.util.ClassEx;
import com.googlecode.cswish.util.ClassUtil;

/**
 * @author Jerry.Feng Date: 2008-4-10
 *
 */
public class PropertyInfo {
	
	private static final Logger logger = Logger.getLogger(PropertyInfo.class);
	
	///////////////////////////// class information ///////////////////////////
	private String name;
	
	private String type;
	
	// available only when it isn't simple object
	private String genericType;
	
	private Method readMethod;
	
	private Method writeMethod;
	
	private boolean isSimpleObj;
	
	//////////////////////////// table information ////////////////////////////
	/**
	 * the field mapped to a table column
	 */
	private boolean tableColumn;
	
	private String tableName;
	
	private String columnName;
	
	// id is generated by hibernate automatically
	private boolean autoId;
	
	private boolean unique;
	
	private boolean nullable;
	
	private boolean insertable;
	
	private boolean updatable;
	
	private int length;
	
	private boolean isDynamic;

	public PropertyInfo() {
	}
	
	public PropertyInfo(String className, String name) {
		setType(className);
		setName(name);
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	public String getTableName() {
		return tableName;
	}

	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	public String getColumnName() {
		return columnName;
	}

	public void setColumnName(String columnName) {
		this.columnName = columnName;
	}

	/**
	 * column type
	 * 
	 * @return
	 */
	public String getType() {
		return type;
	}
	
	public Class<?> getTypeClass() {
		return ClassUtil.forName(type);
	}

	public void setType(String type) {
		this.type = type;
		
		// Default generic type is same with type
		if (this.genericType == null) {
			this.genericType = this.type;
		}
		
		Class<?> typeClass = getTypeClass();
		if (typeClass != null) {
			boolean isSimpleObj = typeClass.isPrimitive() || Number.class.isAssignableFrom(typeClass)
				|| String.class.equals(typeClass) || Date.class.isAssignableFrom(typeClass) 
				|| type.startsWith("java.lang.") || typeClass.isEnum();
			setSimpleObj(isSimpleObj);
		} else {
			setSimpleObj(true);
		}
	}

	public Method getReadMethod() {
		return readMethod;
	}
	
	public Object getValue(Object instance, ModelInfo modelInfo) {
		if (instance == null) {
			return null;
		}
		
		if (instance instanceof HibernateProxy) {
			HibernateProxy hibernateProxy = (HibernateProxy) instance;
			LazyInitializer li = hibernateProxy.getHibernateLazyInitializer();
			if (!li.isUninitialized()) {
				instance = li.getImplementation();
			}
		}
		
		Object value = null;
		if (isDynamic()) {
			if (instance instanceof Map) {
				value = ((Map)instance).get(getName());
			} else {
				try {
					if (instance instanceof IDynamic) {
						try {
							value = readMethod.invoke(instance);
						} catch (IllegalArgumentException ex) {
							if (logger.isInfoEnabled()) {
								logger.info("Found the cached object instance: " + instance);
							}
							// maybe this cached instance created by the old dynamic class
							value = instance.getClass().getMethod(readMethod.getName()).invoke(instance);
						}
					} else {		// type doesn't match
						// use super method to get the value
						ModelInfo parent = modelInfo.getParent();
						PropertyInfo propertyInfo = parent.getProperty(this.name);
						if (propertyInfo != null) {
							value = propertyInfo.getValue(instance, parent);
						}
					}
				} catch (Exception ex) {
					if (logger.isDebugEnabled()) {
						String desc = "Can't read value: " + modelInfo.getSimpleName() + '#' + this.name;
						logger.debug(desc);
					}
				}
			}
		} else if (readMethod != null) {
			try {
				Object[] params = null;
				value = readMethod.invoke(instance, params);
			} catch (Exception ex) {
				if (!readMethod.getDeclaringClass().isAssignableFrom(instance.getClass())) {
					try {
						if (logger.isInfoEnabled()) {
							logger.info("Found the cached object instance: " + instance);
						}
						// maybe this cached instance created by the old dynamic class
						value = instance.getClass().getMethod(readMethod.getName()).invoke(instance);
					} catch (Exception ex2) {
						if (logger.isDebugEnabled()) {
							String desc = "Can't read value: " + modelInfo.getSimpleName() + '#' + this.name;
							logger.debug(desc);
						}
					}
				} else {
					if (logger.isDebugEnabled()) {
						String desc = "Can't read value: " + modelInfo.getSimpleName() + '#' + this.name;
						logger.debug(desc);
					}
				}
			}
		}
		return value;
	}
	
	public void setValue(Object instance, Object value, ModelInfo modelInfo) {
		if (instance == null) {
			return;
		}
		
		if (instance instanceof HibernateProxy) {
			HibernateProxy hibernateProxy = (HibernateProxy) instance;
			LazyInitializer li = hibernateProxy.getHibernateLazyInitializer();
			if (!li.isUninitialized()) {
				instance = li.getImplementation();
			}
		}
		
		if (isDynamic()) {
			if (instance instanceof Map) {
				value = ((Map)instance).put(getName(), value);
			} else {
				if (writeMethod != null) {
					try {
						if (instance instanceof IDynamic) {
							writeMethod.invoke(instance, value);
						} else {		// type doesn't match
							// use super method to get the value
							ModelInfo parent = modelInfo.getParent();
							PropertyInfo propertyInfo = parent.getProperty(this.name);
							if (propertyInfo != null) {
								propertyInfo.setValue(instance, value, parent);
							}
						}
					} catch (Exception ex) {
						if (logger.isDebugEnabled()) {
							String desc = "Can't set value to " + modelInfo.getSimpleName() + '#' + this.name;
							logger.debug(desc);
						}
					}
				}
			}
		} else	if (writeMethod != null) {
			try {
				writeMethod.invoke(instance, value);
			} catch (Exception ex) {
				if (logger.isDebugEnabled()) {
					String desc = "Can't set value to " + modelInfo.getSimpleName() + '#' + this.name;
					logger.debug(desc);
				}
			}
		} else if (instance instanceof Map) {
			((Map)instance).put(this.name, value);
		}
	}

	public void setReadMethod(Method readMethod) {
		this.readMethod = readMethod;
	}

	public Method getWriteMethod() {
		return writeMethod;
	}

	public void setWriteMethod(Method writeMethod) {
		this.writeMethod = writeMethod;
	}

	public boolean isSimpleObj() {
		return isSimpleObj;
	}

	public void setSimpleObj(boolean isSimpleObj) {
		this.isSimpleObj = isSimpleObj;
	}

	public boolean isTableColumn() {
		return tableColumn;
	}

	public void setTableColumn(boolean tableColumn) {
		this.tableColumn = tableColumn;
	}

	public boolean isUnique() {
		return unique;
	}

	public void setUnique(boolean unique) {
		this.unique = unique;
	}

	public boolean isNullable() {
		return nullable;
	}

	public void setNullable(boolean nullable) {
		this.nullable = nullable;
	}

	public boolean isInsertable() {
		return insertable;
	}

	public void setInsertable(boolean insertable) {
		this.insertable = insertable;
	}

	public boolean isUpdatable() {
		return updatable;
	}

	public void setUpdatable(boolean updatable) {
		this.updatable = updatable;
	}

	public int getLength() {
		return length;
	}

	public void setLength(int length) {
		this.length = length;
	}

	public boolean isDynamic() {
		return isDynamic;
	}

	public void setDynamic(boolean isDynamic) {
		this.isDynamic = isDynamic;
	}

	public boolean isId() {
		return "id".equals(this.name);
	}

	public String getGenericType() {
		return ClassUtil.getRuntimeType(genericType);
	}
	
	public Class<?> getGenericTypeClass() {
		try {
			return ClassUtils.getClass(getGenericType());		// TODO: use ClassUtil.forName?
		} catch (ClassNotFoundException ex) {
			logger.debug("generic type is not valid static class:" + genericType);
			return Object.class;
		}
	}

	public void setGenericType(String genericType) {
		if (GenericType.MODEL.equals(genericType) || "java.lang.Object".equals(genericType)) {
			this.genericType = GenericType.MODEL;
		} else if (GenericType.PARAM_MODEL.equals(genericType)) {
			this.genericType = GenericType.PARAM_MODEL;
		} else {
			this.genericType = genericType;
		}
	}
	
	public boolean hasExtendPropertyType() {
		if (this.genericType == null) {
			return false;
		}
		
		boolean hasExtendType;
		if (this.type.equals(this.genericType)) {		// fast check
			hasExtendType = false;
			
			// continue to check
			if (this.readMethod != null) {
				hasExtendType = (this.readMethod.getReturnType() != this.readMethod.getGenericReturnType());
			} else if (this.writeMethod != null) {
				hasExtendType = (this.writeMethod.getParameterTypes()[0] != this.writeMethod.getGenericParameterTypes()[0]);
			}
		} else {
			hasExtendType = true;
		}
		return hasExtendType;
	}
	
	public Class findPropertyType(Class base) {
		Class genericType = null;
		if (this.readMethod != null) {
			genericType = ClassEx.getPropertyType(base, this.readMethod.getGenericReturnType());
		} else if (this.writeMethod != null) {
			genericType = ClassEx.getPropertyType(base, this.writeMethod.getParameterTypes()[0]);
		}
		return genericType;
	}

	public void setAutoId(boolean autoId) {
		this.autoId = autoId;
	}

	public boolean isAutoId() {
		return autoId;
	}
	
	public String toString() {
		return (this.type != null ? this.type : "") + '<' 
			+ (this.genericType != null ? this.genericType : "") + "> "
			+ (this.name != null ? this.name : "");
	}
	
	public PropertyInfo clone() {
		PropertyInfo propertyInfo = new PropertyInfo();
		propertyInfo.setName(this.name);
		propertyInfo.setType(this.type);
		propertyInfo.setSimpleObj(this.isSimpleObj);
		propertyInfo.setGenericType(this.genericType);
		propertyInfo.setAutoId(this.autoId);
		propertyInfo.setDynamic(this.isDynamic);
		propertyInfo.setInsertable(this.insertable);
		propertyInfo.setLength(this.length);
		propertyInfo.setNullable(this.nullable);
		propertyInfo.setReadMethod(this.readMethod);
		
		propertyInfo.setTableColumn(this.tableColumn);
		propertyInfo.setTableName(this.tableName);
		propertyInfo.setColumnName(this.columnName);
		propertyInfo.setUnique(this.unique);
		propertyInfo.setUpdatable(this.updatable);
		propertyInfo.setWriteMethod(this.writeMethod);
		return propertyInfo;
	}
}